\documentclass[12pt]{report}
\usepackage{geometry}
\geometry{a4paper,scale=0.8}
\usepackage{longtable}
\usepackage{url}
\usepackage{listings}
\usepackage{graphicx}
\usepackage{float}
\usepackage{titlesec}
\usepackage{CJK}
\begin{CJK}{UTF8}{gkai}
%\titleformat{\chapter}[hang]{\centering\Huge\bfseries}{\chaptername}{1em}{}
%\renewcommand{\chaptername}{第\CJKnumber{thechapter}章}
\title{A001 学习linux0.01的引导过程}
\author{Chuzy}
\date{2020/05/03}

\lstset{
    basicstyle          =   \small\ttfamily,          % 基本代码风格
    keywordstyle        =   \bfseries,          % 关键字风格
    commentstyle        =   \rmfamily\itshape,  % 注释的风格，斜体
    stringstyle         =   \ttfamily,  % 字符串风格
    flexiblecolumns,                % 别问为什么，加上这个
    numbers             =   left,   % 行号的位置在左边
    showspaces          =   false,  % 是否显示空格，显示了有点乱，所以不现实了
    numberstyle         =   \ttfamily,    % 行号的样式，小五号，tt等宽字体
    showstringspaces    =   false,
    captionpos          =   t,      % 这段代码的名字所呈现的位置，t指的是top上面
    frame               =   lrtb,   % 显示边框
    columns         =   flexible,     
    breaklines      =   true,
    language        =  [ANSI]C,
    escapeinside=``
}


%以上部分叫做"导言区",下面才开始写正文
\begin{document}

%先插入标题
\maketitle
\tableofcontents
\newpage 


\chapter{概述}
尽管0.01版本的linux代码相当简洁，但对初学者来说仍然过于复杂。一种方法是将0.01分解为若干步，每一步完成一个小功能，一步步的理解linux 0.01代码。A001作为的第一步，只验证对boot.s, head.s的理解，以及制作工程和通过bochs进行仿真验证的方法。
\begin{figure}[htbp]
\centering
\includegraphics[scale=0.5]{fig/33.png}
\caption{A001运行结果}
\label{fig:Version A001}
\end{figure}
\chapter{流程图}
\begin{figure}[htbp]
\centering
\includegraphics[scale=0.8]{fig/39.png}
\caption{A001流程}
\label{fig:A001 Flowchart}
\end{figure}
\paragraph{boot/boot.s} 和A000类似，运行在实模式，使用Intel格式的汇编语言编写。在上电后，CPU进入实地址模式，开始执行BIOS。BIOS先进行系统自检，初始化中断向量，然后将可启动设备的磁盘引导扇区读入绝对地址0x7C00（31K）内存处并跳转到这里开始执行代码。boot.s编译产生的代码即引导扇区512字节的内容，在boot.s中完成实模式到保护模式的切换。
\paragraph{boot/head.s} 和A000类似，使用AT\&T 格式的汇编语言编写，用GNU的as编译器（仅支持i386及以后的CPU，不支持生成运行在实模式下的程序）编译。boot.s最后完成跳转时，将控制权交head.s。在head.s中，将设置内存分页模式，使用分页内存机制。
\paragraph{init/main.c} 在A001中，我们增加了用c语言编写的初始化部分，head.s中激活内存分页模式后，把控制权交给c语言编写的main函数，在main函数中，我们暂时什么也不做。
\paragraph{tools/build.c} 在A000中，我们使用objcopy命令将as+ld编译的head.s产生的system，转成binary形式，串接在boot后面。本章我们自己构造一个build工具，剥离取出gcc+ld编译出来的elf文件头后，将boot和system串接成image文件。
\paragraph{Makefile} 工程文件，在A000中，我们只有汇编格式的源文件，且放在一个目录下，本章，我们会有不同的目录，而且将会编译c代码。

\chapter{引导扇区源码 boot/boot.s}
加电自检结束后，boot.s 的代码被加载到 0x7C00（31k）处执行，其首先将自身移动到物理地址的 0x90000（576k）处，并跳转到该处执行，用BIOS中断在屏幕上打印出 “Loading system...”，然后将系统映像从磁盘读取到0x10000（64k）处，再从0x10000复制到0x0处, 最后设置并进入保护模式后跳转到0x0地址。
\section{移动boot自身到0x90000(576k)}
\begin{lstlisting}
!
!	boot.s
!
! boot.s is loaded at 0x7c00 by the bios-startup routines, and moves itself
! out of the way to address 0x90000, and jumps there.
!
! It then loads the system at 0x10000, using BIOS interrupts. Thereafter
! it disables all interrupts, moves the system down to 0x0000, changes
! to protected mode, and calls the start of system. System then must
! RE-initialize the protected mode in it's own tables, and enable
! interrupts as needed.
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough - in fact more would mean we'd have to move
! not just these start-up routines, but also do something about the cache-
! memory (block IO devices). The area left over in the lower 640 kB is meant
! for these. No other memory is assumed to be "physical", ie all memory
! over 1Mb is demand-paging. All addresses under 1Mb are guaranteed to match
! their physical addresses.
!
! NOTE1 abouve is no longer valid in it's entirety. cache-memory is allocated
! above the 1Mb mark as well as below. Otherwise it is mainly correct.
!
! NOTE 2! The boot disk type must be set at compile-time, by setting
! the following equ. Having the boot-up procedure hunt for the right
! disk type is severe brain-damage.
! The loader has been made as simple as possible (had to, to get it
! in 512 bytes with the code to move to protected mode), and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.

! 1.44Mb disks: 2Head * 80Track * 18Sector = 2880Sector * 512 = 1440 KB
sectors = 18
! 1.2Mb disks:
! sectors = 15
! 720kB disks:
! sectors = 9

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

BOOTSEG = 0x07c0
INITSEG = 0x9000
SYSSEG  = 0x1000			! system loaded at 0x10000 (65536).
ENDSEG	= SYSSEG + SYSSIZE	! `SYSSIZE将由Makefile根据编译后的内核长度计算并填入`

entry start
start:
	mov	ax,#BOOTSEG
	mov	ds,ax
	mov	ax,#INITSEG
	mov	es,ax
	mov	cx,#256		! `256个word, 512字节`
	sub	si,si
	sub	di,di
	rep
	movw
	jmpi	go,INITSEG
go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax
	mov	ss,ax
	mov	sp,#0x400	! arbitrary value >>512

	mov	ah,#0x03	! read cursor pos
	xor	bh,bh
	int	0x10		! `BIOS中断`
	
	mov	cx,#20		! `20个字符`
	mov	bx,#0x0007	! page 0, attribute 7 (normal)
	mov	bp,#msg1	! `显示加载信息`
	mov	ax,#0x1301	! write string, move cursor
	int	0x10
\end{lstlisting}	
\section{从磁盘读取系统映像到并搬移到0x0地址}
\begin{lstlisting}	
! ok, we've written the message, now
! we want to load the system (at 0x10000)

	mov	ax,#SYSSEG
	mov	es,ax		! segment of 0x010000
	call	read_it
	call	kill_motor

! if the read went well we get current cursor position ans save it for
! posterity.

	mov	ax,cs
	mov	es,ax

	mov	ah,#0x03	! read cursor pos
	xor	bh,bh
	int	0x10		

	mov	cx,#9
	mov	bx,#0x0007
	mov	bp,#msg2	! `显示加载完成`
	mov	ax,#0x1301
	int	0x10

	mov	ah,#0x03
	xor	bh,bh
	int	0x10
	mov	[510],dx	! `保存屏幕光标位置到0x90510.`

! now we want to move to protected mode ...

	cli			! no interrupts allowed !

! first we move the system to it's rightful place

	mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000	! `复制8次`
	jz	end_move
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000	! `一次复制0x8000*2=64k字节`
	rep
	movsw
	j	do_move
\end{lstlisting}	
\section{进入保护模式}
\begin{lstlisting}			
! then we load the segment descriptors

end_move:

	mov	ax,cs		! right, forgot this at first. didn't work :-)
	mov	ds,ax
	lidt	idt_48		! load idt with 0,0
	lgdt	gdt_48		! load gdt with whatever appropriate

! that was painless, now we enable A20 `通过键盘端口开A20地址线`

	call	empty_8042
	mov	al,#0xD1		! command write
	out	#0x64,al
	call	empty_8042
	mov	al,#0xDF		! A20 on
	out	#0x60,al
	call	empty_8042

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F(`IRQ0对应0x20`). There they won't mess up anything. 
! Sadly IBM really messed this up with the original PC,
! and they haven't been able to rectify it afterwards. Thus the bios puts 
! interrupts at 0x08-0x0f (`IRQ0对应0x08`),
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.

	mov	al,#0x11		! initialization sequence
	out	#0x20,al		! send it to 8259A-1
	.word	0x00eb,0x00eb		! `相当于`jmp $+2, jmp $+2
	out	#0xA0,al		! and to 8259A-2
	.word	0x00eb,0x00eb
	mov	al,#0x20		! start of hardware int's (0x20)
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28		! start of hardware int's 2 (0x28)
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x04		! 8259-1 is master
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x02		! 8259-2 is slave
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x01		! 8086 mode for both
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0xFF		! mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.

	mov	ax,#0x0001	! protected mode (PE) bit
	lmsw	ax		! This is it!
	jmpi	0,8		! jmp offset 0 of segment 8 (cs)
\end{lstlisting}	
\section{检查键盘缓冲区函数empty\_8042}
\begin{lstlisting}			
! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
empty_8042:
	.word	0x00eb,0x00eb
	in	al,#0x64	! 8042 status port
	test	al,#2		! is input buffer full?
	jnz	empty_8042	! yes - loop
	ret
\end{lstlisting}	
\section{系统映像读取函数read\_it}
\paragraph{1.44M软盘}
1.44M软盘有2 head，80 track，18 sector组成。为了提高读取效率，一次读取一个track (18个扇区)，且在0面，0 track后，跟着的是1面，0 track。
\paragraph{ok1\_read}
当(ax-sread)左移9bit，即待读取扇区数*512等于待读取字节数，加上bx，读取的目的offset超过16bit寄存器边界(64k边界)时，需要建少读取的扇区数，bs+要读取的字节数，不超过64k的地址边界，即在当前段寄存器的寻址范围内。sread的初始值为1,即第一次读取时，假设已经读取了一个扇区(boot扇区)。
\paragraph{ok2\_read}
调用read\_track读一个完整的track, 下一个要读的track位于另外一次head，如果两个head的同一条track都已经读取，则track号也需要+1。即当前head=1, 下次head=0, track保持不变；如果当前head=1, 则下次head=0, track=tack+1。
\paragraph{ok3\_read}
记录当前读取的sector，要么是中间停止到某个sector，要么是新的一个track的开始；根据读取到的字节数，更新读取的目的地址的offset，如果遇到64K的边界（64K/512/18 = 64/9，一定会遇到），要改变es段地址，并把offset置为0。然后回到rp\_read开始下一次的读取。
\paragraph{ok4\_read}
更新磁头号。
\paragraph{read\_track}
读取一整个track的18个sector。

\begin{lstlisting}			
! This routine loads the system at address 0x10000, making sure
! no 64kB boundaries are crossed. We try to load it as fast as
! possible, loading whole tracks whenever we can.
!
! in:	es - starting address segment (normally 0x1000)
!
! This routine has to be recompiled to fit another drive type,
! just change the "sectors" variable at the start of the file
! (originally 18, for a 1.44Mb drive)
!
sread:	.word 1			! sectors read of current track
head:	.word 0			! current head
track:	.word 0			! current track
read_it:
	mov ax,es		! `段基址<<4 + offset等于物理地址`
	test ax,#0x0fff		! `和64k(0x10000)边界对应的段基址的低12bit须全0`
die:	jne die			! es must be at 64kB boundary
	xor bx,bx		! bx is starting address within segment
rp_read:
	mov ax,es
	cmp ax,#ENDSEG		! have we loaded all yet?
	jb ok1_read
	ret
ok1_read:
	mov ax,#sectors		! `一次读取一个track，对于1.44M磁盘，18个sector`
	sub ax,sread		! `减去该track已经读取的sector数`
	mov cx,ax
	shl cx,#9		! `由sector数得到字节数`
	add cx,bx		! `预测全部读取后的目的地址`
	jnc ok2_read		! `如果寄存器没溢出，说明目的地址还在段内，正常读取`
	je ok2_read
	xor ax,ax		
	sub ax,bx		! `如果溢出，计算该段还能读取多少字节`
	shr ax,#9		! `把要读取的字节数换算成要读取的扇区数`
ok2_read:
	call read_track		! `读取一个track，cx返回实际读取的sector数`
	mov cx,ax		
	add ax,sread		
	cmp ax,#sectors		! `是否完成了一个track的读取`
	jne ok3_read		! `未完成track则不需要更新，进入ok3`
	mov ax,#1		
	sub ax,head		! `判断是否磁头1`
	jne ok4_read		! `不是则不需要更新track后，进入ok4,更新head号`
	inc track		! `是head1,则需要更新track号`
ok4_read:
	mov head,ax		! 
	xor ax,ax		! `更新head号，由0更新到1,或者由1更新到0`
ok3_read:
	mov sread,ax		! `保存当前track已读取的sector值`
	shl cx,#9		! `计算本次已读取的字节数`
	add bx,cx		! `更新段内的offset值`
	jnc rp_read		! `若还未越64k边界，则继续读`
	mov ax,es		 
	add ax,#0x1000		
	mov es,ax		! `更新段寄存器的值`
	xor bx,bx		! `更新段内offset的值`
	jmp rp_read		

read_track:
	push ax
	push bx
	push cx
	push dx
	mov dx,track
	mov cx,sread
	inc cx
	mov ch,dl
	mov dx,head
	mov dh,dl
	add cx,bx
	mov dl,#0
	and dx,#0x0100
	mov ah,#2
	int 0x13
	jc bad_rt
	pop dx
	pop cx
	pop bx
	pop ax
	ret
bad_rt:	mov ax,#0
	mov dx,#0
	int 0x13
	pop dx
	pop cx
	pop bx
	pop ax
	jmp read_track
\end{lstlisting}	
\section{关闭软驱函数kill\_motor}
\begin{lstlisting}			
!/*
! * This procedure turns off the floppy drive motor, so
! * that we enter the kernel in a known state, and
! * don't have to worry about it later.
! */
kill_motor:
	push dx
	mov dx,#0x3f2
	mov al,#0
	outb
	pop dx
	ret
\end{lstlisting}	
\section{GDT/IDT等数据区}
\begin{lstlisting}	
gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386

idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L

gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	gdt,0x9		! gdt base = 0X9xxxx
	
msg1:
	.byte 13,10
	.ascii "Loading system ..."
msg2:
	.ascii "done."
	.byte 13,10,13,10
.text
endtext:
.data
enddata:
.bss
endbss:
\end{lstlisting}

\chapter{系统底层初始化源码 boot/head.s}
boot.s实现计算机加电自检引导扇区，第一次加载扇区和第二次加载操作系统的功能，head.s 主要包括重新设置GDT/IDT, 检查A20地址线，数据协处理器芯片，设置管理内存的分页处理机制，最后利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出，将控制权转交给main函数。

\section{重新初始化GDT/IDT}	
首先使用boot.s中的设置的GDT初始化段寄存器，并完成对初始化程序的调用；然后重新装入新的GDT/IDT，重新初始化段寄存器和堆栈。
\begin{lstlisting}
/*
 *  head.s contains the 32-bit startup code.
 *
 * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
 * the page directory will exist. The startup code will be overwritten by
 * the page directory.
 */
.text
.globl idt,gdt,pg_dir,startup_32
pg_dir:
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	call setup_idt
	call setup_gdt
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
\end{lstlisting}
\section{检查A20地址线和CR0寄存器}	
\paragraph{CR0～CR3}
用于控制和确定处理器的操作模式以及当前执行任务的特性，CR0中含有控制处理器操作模式和状态的系统控制标志；CR1保留不用；CR2含有导致页错误的线性地址；CR3中含有页目录表物理内存基地址，也被称为页目录基地址寄存器PDBR（Page-Directory Base address Register）。
\paragraph{CR0:PE}
PE: CR0的位0是启用保护（Protection Enable）标志。当设置该位时即开启了保护模式, 这个标志仅开启段级保护，而并没有启用分页机制。若要启用分页机制，那么PE和PG标志都要置位。在修改该了PE位之后程序必须立刻使用一条跳转指令，以刷新处理器执行管道中已经获取的不同模式下的任何指令。
\paragraph{CR0:EM}
CR0的位2是仿真（EMulation）标志。当该位设置时，表示处理器没有内部或外部协处理器，执行协处理器指令时会引起设备不存在异常；当清除时，表示系统有协处理器。设置这个标志可以迫使所有浮点指令使用软件来模拟。
\paragraph{CR0:ET}
ET：CR0的位4是扩展类型（Extension Type）标志。当该标志为1时，表示指明系统中有80387协处理器，并使用32位协处理器协议。ET=0指明使用80287协处理器。如果仿真位EM=1，则该位将被忽略。
\paragraph{CR0:PG}
PG：CR0的位31是分页（Paging）标志。当设置该位时即开启了分页机制；当复位时则禁止分页机制，此时所有线性地址等同于物理地址。在开启这个标志之前必须已经或者同时开启PE标志。只有当执行程序至少有部分代码和数据在线性地址空间和物理地址空间中具有相同地址时，我们才能改变PG位的设置。此时这部分具有相同地址的代码在分页和未分页世界之间起着桥梁的作用。另外，在开启分页（PG=1）之前必须先刷新页高速缓冲TLB。
\begin{lstlisting}
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000
	cmpl %eax,0x100000
	je 1b			# `A20若正常开启，则必然不相等，否则进行循环`
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,ET,PE
	testl $0x10,%eax
	jne 1f			# ET is set - 387 is present
	orl $4,%eax		# else set emulate bit
1:	movl %eax,%cr0		# `PG=原来的初始值,ET=1 EM=0,PE=原来的初始值`
	jmp after_page_tables
\end{lstlisting}
\section{函数setup\_idt/setup\_idt}	
\begin{lstlisting}
/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr
	ret
\end{lstlisting}
\section{页表空间}	
\paragraph{pg\_dir} 定义在head.s最开始处，即从0地址开始，到0x1000结束，占用了4K字节。每个页表需要4个字节，因此一个页目录共可以存放1024个页表。
\paragraph{pg0} 从0x1000开始，到0x2000结束，占用了4K字节。每个页表项需要4个字节，因此一个页共可以存放1024个页表项。一个页表项对应4k内存，一个页表寻址4M内存。
\paragraph{pg1} 
从0x2000开始，到0x3000结束，占用了4K字节,寻址4M内存。
\paragraph{pg2} 
从0x3000开始，到0x4000结束，占用了4K字节,寻址4M内存, linux0.01版本的内核未使用到。
\begin{lstlisting}
.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:		# This is not used yet, but if you
		# want to expand past 8 Mb, you'll have
		# to use it.

.org 0x4000
\end{lstlisting}
\section{页表之后}	
\paragraph{after\_page\_tables} 
从0地址到after\_page\_tables之间的内存空间都会被作为页表空间，在页表建立过程中被初始化。after\_page\_tables之后的内容将会被保持。
\begin{lstlisting}
after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging	# `在setup\_paging中，初始化页表后执行ret指令`
	L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.
\end{lstlisting}
\section{通用中断处理函数ignore\_int}	
\begin{lstlisting}
/* This is the default interrupt "handler" :-) */
.align 4
ignore_int:
	incb 0xb8000+160		# put something on the screen
	movb $2,0xb8000+161		# so that we know something
	iret				# happened
\end{lstlisting}
\section{建立页表，ret到main}	
\paragraph{开启分页机制} setup\_paging是从after\_page\_tables跳转而来的，这里的ret，直接返回到前面压栈的main函数的地址。ret指令同时也刷新了prefetch-queue，内存寻址模式开始采用分页方式。
\paragraph{分页机制下的内存转换} 在分页模式下，段选择符+offset转换得到的线性地址，还需要经过分页机制的转换，才能得到物理地址。一个32位的线性地址被分为3个部分：31-22共10个bit用来确定页目录中的目录项，21-12用来寻址页目录指定的页表中的页表项，最后的12bit用作指定一页物理内存中的偏移地址。
\paragraph{对内核代码的影响} 由于内核的代码段和数据段都是从物理0地址开始，而分页的pg\_dir和 pg0/1 也是从0地址开始，内核的代码段和数据段地址转换得到的线性地址经分页转换后不变。
\begin{lstlisting}
/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 8MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 8 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 8Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "8Mb"), but I
 * won't guarantee that's all :-( )
 */
.align 4
setup_paging:
	movl $1024*3,%ecx
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl			# `清空pg\_dir，pg0和pg1, 从0开始的3k空间`
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/* `在pg\_dir，增加pg0和pg1两个页` */
	movl $pg1+4092,%edi		/*  `从pg1结束的位置，开始反向填充` */
	movl $0x7ff007,%eax		/*  8Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax	/*  `每个页表项寻址4k，前项的基地址比后项小0x1000` */
	jge 1b
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */
\end{lstlisting}
\section{GDT/IDT等数据区}	
\begin{lstlisting}
.align 4
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long idt
.align 4
.word 0
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)

.align 8
idt:	.fill 256,8,0		# idt is uninitialized

gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a00000007ff	/* 8Mb */
	.quad 0x00c09200000007ff	/* 8Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */
\end{lstlisting}
\chapter{系统高层初始化源码 init/main.c}
main.c目的是让整个内核跑起来。这里定义了stack\_start， head.s用stack\_start来初始化ss和esp。
\section{入口函数 main}
\begin{lstlisting}[breaklines]
#define PAGE_SIZE 4096
long user_stack [ PAGE_SIZE>>2 ] ;
struct {
        long * a;
        short b;
        } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

int main(void)
{
    for(;;);
    return 0;
}
\end{lstlisting}
\chapter{构建系统映像的工具源码 tools/build.c}
\paragraph{1}
build是一个独立的程序，其主要工作是去掉head.s，main.c等编译出来的内核system文件的MINIX执行文件头结构信息，然后把system连接在boot后面，顺序写入名为Image的文件。
\paragraph{2}
build使用两个参数，分别是boot的文件名和system文件名，
\paragraph{3} 使用gcc 9.3版本编译生成的System文件格式和Linux0.01发布时的格式差别相当大，原来的方法不能使用，这里采用的方法是将代码段和数据段从System文件中提取出来。
\paragraph{4} 首先从文件头中，找到Secssion的头，然后根据Secssion的头，找到.text和.data在文件的offset和在内存中实际地址，再将从文件offset读取到的内容，写入Image文件对应的位置（512+将被加载到的内存地址）。
\section{读取/复制boot}
先读取MINIX\_HEADER长度，并检查内容是否正确，然后跳过头部内容，将去掉MINIX头的boot部分，写入Image，并增加0xAA55标识。
\begin{lstlisting}[breaklines]
#include <stdio.h>	/* fprintf */
#include <stdlib.h>	/* contains exit */
#include <sys/types.h>	/* unistd.h needs this */
#include <unistd.h>	/* contains read/write */
#include <fcntl.h>
#include <malloc.h>
#include </usr/include/elf.h>

#define MINIX_HEADER 32
#define GCC_HEADER 4096

void die(char * str)
{
	fprintf(stderr,"%s\n",str);
	exit(1);
}

void usage(void)
{
	die("Usage: build boot system [> image]");
}

int main(int argc, char ** argv)
{
	int i,c,id;
	char buf[4096];
	
	Elf32_Ehdr ElfHead;
	Elf32_Ehdr * pElfHead = &ElfHead;

	int lenShdr;
	Elf32_Shdr * pShdr;

	char * pNameTab=0;
	char * pName;

	char * pWritebuf;

	if (argc != 3)
		usage();
	for (i=0;i<sizeof buf; i++) buf[i]=0;
	if ((id=open(argv[1],O_RDONLY,0))<0)
		die("Unable to open 'boot'");
	if (read(id,buf,MINIX_HEADER) != MINIX_HEADER)
		die("Unable to read header of 'boot'");
	if (((long *) buf)[0]!=0x04100301)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[1]!=MINIX_HEADER)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[3]!=0)
		die("Illegal data segment in 'boot'");
	if (((long *) buf)[4]!=0)
		die("Illegal bss in 'boot'");
	if (((long *) buf)[5] != 0)
		die("Non-Minix header of 'boot'");
	if (((long *) buf)[7] != 0)
		die("Illegal symbol table in 'boot'");
	i=read(id,buf,sizeof buf);
	fprintf(stderr,"Boot sector %d bytparagraph{1}es.\n",i);
	if (i>510)
		die("Boot block may not exceed 510 bytes");
	buf[510]=0x55;
	buf[511]=0xAA;
	i=write(1,buf,512);
	if (i!=512)
		die("Write call failed");
	close (id);
\end{lstlisting}	
\section{读取/复制System}	
\paragraph{1} 读取头部4k字节，检查GCC头是否正确。
\begin{lstlisting}[breaklines]
	if ((id=open(argv[2],O_RDONLY,0))<0)
		die("Unable to open 'system'");
	if (read(id,buf,GCC_HEADER) != GCC_HEADER)
		die("Unable to read header of 'system'");
	if (((long *) buf)[6] != 0)
		die("Non-GCC header of 'system'");
\end{lstlisting}	
\paragraph{2} 获得elf文件头。
\begin{lstlisting}[breaklines]
	
	ElfHead = *((Elf32_Ehdr * )buf);
	fprintf(stderr, "%s", "ELF Header:\n");
	fprintf(stderr, "%s","Magic:");
	for(i=0; i<EI_NIDENT; i++)
	{
		fprintf(stderr, " 0x%x", pElfHead->e_ident[i]);
	}
	fprintf(stderr, "%s", "\n");
	fprintf(stderr, "Entry Point:\t0x%x\n", pElfHead->e_entry);
	fprintf(stderr, "S Header offet:\t0x%x\n", pElfHead->e_shoff);	
	fprintf(stderr, "S Header size:\t0x%x\n", pElfHead->e_shentsize);
	fprintf(stderr, "S Header num:\t0x%x\n", pElfHead->e_shnum);
	fprintf(stderr, "S Index:\t0x%x\n", pElfHead->e_shstrndx);
\end{lstlisting}	
\paragraph{3} 根据elf文件头中的Session头的信息，读取Session头。
\begin{lstlisting}[breaklines]

	lenShdr = pElfHead->e_shnum * pElfHead->e_shentsize;
	pShdr = (Elf32_Shdr *)malloc(lenShdr);
	if(lseek(id,pElfHead->e_shoff,SEEK_SET) >= 0)
	{
		read(id, pShdr, lenShdr);
	}
	else
	{
		die("Read Sesstion header failed!\n");
	}
\end{lstlisting}	
\paragraph{4} 找到字符表对应的Session。
\begin{lstlisting}[breaklines]

	for(i=0; i<pElfHead->e_shnum; i++)
        {
		if(((pShdr[i]).sh_type) == SHT_STRTAB)
		{
			pNameTab = (char *)malloc((pShdr[i]).sh_size);
			if(lseek(id,(pShdr[i]).sh_offset,SEEK_SET) >= 0)
		 	{
				read(id, pNameTab, (pShdr[i]).sh_size);
			}
			else
			{
				die("Read Sesstion header failed!\n");
			}
			break;
		}
        }

	if(pNameTab == 0)
	{
		die("No SHT_STRTAB found!\n");
	}
\end{lstlisting}	
\paragraph{5} 找到.data和.text对应的Session, offset对应段在源文件中的位置, addr则对应段应该被读取到内存中位置。由于boot代码将紧随其后system代码加载到0地址，因此addr=0,就对应到512字节处。
\begin{lstlisting}[breaklines]

	for(i=0; i<pElfHead->e_shnum; i++)
	{
		pName = &(pNameTab[(pShdr[i]).sh_name]);
		fprintf(stderr,"[%d]",i);
		fprintf(stderr,"\t0x%x",(pShdr[i]).sh_type);
		fprintf(stderr,"\t0x%x",(pShdr[i]).sh_offset);
		fprintf(stderr,"\t0x%x",(pShdr[i]).sh_addr);
		fprintf(stderr,"\t0x%x",(pShdr[i]).sh_size);
		fprintf(stderr,"\t%s",pName);
		fprintf(stderr,"%s","\n");

		if(((strcmp(pName, ".data")==0) || (strcmp(pName, ".text")==0)) || (strcmp(pName, ".rodata")==0)))
                {
                        pWritebuf = (char *)malloc((pShdr[i]).sh_size);
                        if((lseek(id,(pShdr[i]).sh_offset,SEEK_SET) >= 0)
				&&(lseek(1,(pShdr[i]).sh_addr+512,SEEK_SET) >= 0))
                        {
                                if((c=read(id, pWritebuf, (pShdr[i]).sh_size)) > 0)
				{
					write(1,pWritebuf,c);
					fprintf(stderr,"Write System %s %d bytes.\n",pName, c);
				}
			}
			else
			{
				die("Seek Sesstion data failed!\n");
			}
		}
	}

	close(id);
	close(1);
	return(0);
}
\end{lstlisting}
\chapter{工程文件 Makefile}
\section{Makefile}
\begin{lstlisting} [breaklines]
AS86    =as86 -0 -a
LD86    =ld86 -0

AS      =as --32
LD      =ld -melf_i386 -e startup_32 -Ttext 0
LDFLAGS =-s -x -M
CC      =gcc -m32
CFLAGS  =-Wall -O -fstrength-reduce -fomit-frame-pointer
CPP     =gcc -E -nostdinc -Iinclude
.c.s:
        $(CC) $(CFLAGS) \
        -nostdinc -Iinclude -S -o $*.s $<
.s.o:
        $(AS) -c -o $*.o $<
.c.o:
        $(CC) $(CFLAGS) \
        -nostdinc -Iinclude -c -o $*.o $<
\end{lstlisting}
\subsection{生成Image}
Image通过build连接boot和system生成。
\begin{lstlisting} [breaklines]
all:    Image
Image:  boot/boot tools/system tools/build
        tools/build boot/boot tools/system > Image
        sync
\end{lstlisting}
\subsection{编译生成build}
\begin{lstlisting} [breaklines]
tools/build: tools/build.c
        $(CC) $(CFLAGS) \
        -o tools/build tools/build.c
\end{lstlisting}
\subsection{编译生成system}
system由head.s和main.c编译连接生成。
\begin{lstlisting} [breaklines]
boot/head.o: boot/head.s

tools/system:   boot/head.o init/main.o \
                $(ARCHIVES) $(LIBS)
        $(LD) $(LDFLAGS) boot/head.o init/main.o \
        -o tools/system > System.map
\end{lstlisting}
\subsection{编译生成boot}
tmp.sh读取编译之后的system的长度，并在读取boot.s源代码的基础上更新SYSSIZE的定义，生成tmp.s，编译tmp.s生成boot。
\begin{lstlisting} [breaklines]        
boot/boot:      boot/boot.s tools/system
        (./tmp.sh)
        $(AS86) -o boot/boot.o tmp.s
        rm -f tmp.s
        $(LD86) -s -o boot/boot boot/boot.o
\end{lstlisting}
\subsection{其他make选项}
\begin{lstlisting} [breaklines]
clean:
        rm -f Image System.map tmp_make boot/boot core
        rm -f init/*.o boot/*.o tools/system tools/build
backup: clean
        (cd .. ; tar cf - linux | compress16 - > backup.Z)
        sync

dep:
        sed '/\#\#\# Dependencies/q' < Makefile > tmp_make
        (for i in init/*.c;do echo -n "init/";$(CPP) -M $$i;done) >> tmp_make
        cp tmp_make Makefile                                                                                                 
\end{lstlisting}

\section{tmp.sh}
\paragraph{1} 当内核tools/system编译出来后，要加载的内核长度就确定下来了，tmp.sh就是要取出这个值，并补充到boot.s中。
\paragraph{2} 
tmp.sh中，获取文件长度后，完成SYSSIZE的计算，将SYSSIZE=xxxxx连接到原来的boot.s中，形成新的文件tmp.s。
\paragraph{3} tmp.sh是原来0.01的工程中没有的。产生这个文件的原因是现在的as86不认识SYSSIZE= (SYSSIZE + 15) / 16))这样的表达式，只能处理SYSSIZE=xxxxx这样的方式。
\begin{lstlisting} [breaklines]
#!/bin/sh

SYSSIZE=$(ls -l tools/system \
        | grep system | cut -c25-31 | tr '\012' ' ')
SYSSIZE=$((($SYSSIZE + 15) / 16))

echo -n "SYSSIZE = " $SYSSIZE "\n"> tmp.s
cat boot/boot.s >> tmp.s
\end{lstlisting}

\chapter{模拟运行}
\section{断点设置}
\paragraph{1} 实模式断点：
我们知道仿真代码的第一个引导扇区被加载到了0x0000:0x7c00，这是一个实模式的地址，如果我们希望从这点开始中断，在进入bochs之后的暂停状态，就不能敲入c了，而是设置断点，然后再敲入c。
\begin{lstlisting} [breaklines]
vb 0x0000:0x7c00
\end{lstlisting}
\paragraph{2} {保护模式断点}
实模式的boot代码将内核读到0地址后，进入保护模式，如果我们希望直接在保护模式0地址开始调试，则断点设置方式为：
\begin{lstlisting} [breaklines]
vb 0x0008:0x00000
\end{lstlisting}
\section{继续、单步和Step}
\paragraph{1} 在设置断点后，命令c将继续程序运行直至断点；在中断后，使用命令n进行单步跟踪；如遇到函数调度，可用命令s，跟踪到函数内部。
\section{寄存器查看}
\paragraph{1} 在中断后，使用命令r查看寄存器状态
\begin{lstlisting} [breaklines]
eax: 0x00001301 4865
ecx: 0x00000018 24
edx: 0x00001100 4352
ebx: 0x00000007 7
esp: 0x00000400 1024
ebp: 0x000001bd 445
esi: 0x000e0200 918016
edi: 0x00000200 512
eip: 0x00000035
eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
\end{lstlisting}

\end{CJK}
\end{document}
